import NIM from './vender/NIM_Web_NIM_v6.2.0'

import Consts from '../assets/consts'

import Log from './log'
import db from './db'
import foo from './foo'
import DelayPromise from './delay.promise'

let dp = new DelayPromise()
let log = new Log('nim>')

const what = Consts.im.what
const arg1 = Consts.im.arg1
const arg2 = Consts.im.arg2
const custom = Consts.im.custom
const imdb = Consts.im.db
const imtables = Consts.im.db.tables

let sdk = null

let _msg_cb = null
let _handler = null

let synced = false
let connected = false
let currentAccount = ''

let userDate = {}

let resetLocal = _ => {
  _msg_cb = null
  _handler = null

  synced = false
  connected = false
  currentAccount = ''

  userDate = {}
}

let cb = {
  onLog: data => {
    log.v('onLog', data)
  },

  onSendDone: (err, obj) => {
    log.v('onSendDone', obj, err)

    const msg = foo.msgTodo(obj)
    const key = msg.custom[custom.dpKey]

    if (!err) {
      db.open(imdb.prefix + currentAccount)
      .then(e => db.add(e, imtables.message.name, msg))
      .then(e => db.update(e,
          imtables.session.name, msg.session, (v) => {
            if (null == v) {
              v = foo.msg4session(msg)
            }

            v.unread = 0
            v.ts = v.ts > msg.ts ? v.ts : msg.ts
            v.message = msg

            return v
          }))
      .then(_ => dp.done(key, err, msg))
      .catch(e => dp.done(key, e))
    } else {
      dp.done(key, err)
    }
  },

  onConnect: data => {
    log.v('onConnect', data)

    synced = false;
    connected = true;

    dp.done('im-connect', null, data);
    foo.handle(_handler, what.connected, arg1.success, arg2.normal, data)
  },

  onWillReconnect: data => {
    log.v('onWillReconnect', data)
    foo.handle(_handler, what.disconnectWillConnect,
        arg1.success, arg2.normal, data)
  },

  // callFunc, code, event, message, timetag, willReconnect
  onDisconnect: data => {
    log.v('onDisconnect', data)

    dp.done('im-disconnect', null, data)
    foo.handle(_handler, what.disconnect, arg1.success, arg2.normal, data)

    resetLocal()
  },

  onError: e => log.e('onError', e),
  onLoginPortsChange: data => log.w('onLoginPortsChange', data),

  // onSyncMarkinBlacklist: data => log.d('onSyncMarkinBlacklist', data),
  // onMuteList: list =>log.d('onMuteList', list),
  // onSyncMarkinMuteList: data => log.d('onSyncMarkinMuteList', data),

  // teamId, owner, createTime, memberNum, mute, muteType, name, valid, validToCurrentUser
  onTeams: list => log.v('onTeams', list),
  onSyncCreateTeam: data => log.v('onSyncCreateTeam', data),
  onTeamMembers: data => log.v('onTeamMembers', data),
  onSyncTeamMembersDone: data => log.v('onSyncTeamMembersDone', data),
  onUpdateTeamMember: data => log.v('onUpdateTeamMember', data),

  onCreateTeam: data => log.v('onCreateTeam', data),
  onUpdateTeam: data => {
    log.v('onUpdateTeam', data)
    foo.handle(_handler, what.notify,
        arg1.success, arg2.teamUpdate, foo.teamCbTodo(data))
  },

  onAddTeamMembers: data => {
    log.v('onAddTeamMembers', data)

    const t = foo.teamCbTodo(data)
    const sid = 'team-' + t.team

    db.open(imdb.prefix + currentAccount)
    .then(e => db.update(e, imtables.session.name, sid,
        v => (null == v) ? foo.id4session(sid) : null))

    // [Neo] .add me into team
    .then(_ => foo.handle(_handler, what.notify,
        arg1.success, arg2.teamChangeForSessionUpdate, t))

    // [Neo] .add new member for team
    .catch(_ => foo.handle(_handler, what.notify,
        arg1.success, arg2.teamAddMembers, t))
  },

  onUpdateTeamManagers: data => {
    log.w('onUpdateTeamManagers ', data)
    foo.handle(_handler, what.notify,
        arg1.success, arg2.teamUpdateManagers, foo.teamCbTodo(data))
  },

  onRemoveTeamMembers: data => {
    log.v('onRemoveTeamMembers', data)

    const t = foo.teamCbTodo(data)
    const sid = 'team-' + t.team

    db.open(imdb.prefix + currentAccount)
    .then(e => db.get(e, imtables.session.name, sid))
    .then(e => {
      let deleteMe = false
      e.result.accounts.forEach(a => {
        if (currentAccount === a) {
          deleteMe = true
        }
      })

      if (deleteMe) {
        return db.delete(e, imtables.session.name, sid)
      } else {
        return dp.bad()
      }
    })

    // [Neo] .kick me from team
    .then(_ => foo.handle(_handler, what.notify,
        arg1.success, arg2.teamChangeForSessionUpdate, t))

    // [Neo] .kick other
    .catch(_ => foo.handle(_handler, what.notify,
        arg1.success, arg2.teamRemoveMembers, t))

  },

  onUpdateTeamMembers: data => {
    log.v('onUpdateTeamMembers', data)
    foo.handle(_handler, what.notify,
        arg1.success, arg2.teamUpdateMembers, foo.teamCbTodo(data))
  },

  onDismissTeam: data => {
    log.v('onDismissTeam', data)
    // [Neo] .TODO session.delete
    foo.handle(_handler, what.notify,
        arg1.success, arg2.teamDismiss, foo.teamCbTodo(data))
  },

  onTransferTeam: data => {
    log.v('onTransferTeam', data)
    // [Neo] .TODO session.delete
    foo.handle(_handler, what.notify,
        arg1.success, arg2.teamTransfer, foo.teamCbTodo(data))
  },

  onUpdateTeamMembersMute: data => log.v('onUpdateTeamMembersMute', data),

  onSessions: list => {
    log.v('onSessions', list)

    db.open(imdb.prefix + currentAccount)
    .then(e => {
      let ss = list.map(foo.sessionTodo)
      db.updateBatch(e, imtables.session.name, ss.map(l => l.session), ss,
          (old, s) => {
            let session = null != old ? old : s

            if (s.ts > session.ignore) {
              s.ignore = session.ignore
              s.pin = session.pin
              s.mute = session.mute

              session = s
            }

            return session
          })
    })
    .catch(e => log.e(e))
  },

  onUpdateSession: data => {
    // [Neo] .TODO update session db
    log.v('onUpdateSessions', data)
  },

  // attach: type ...
  // type, flow, scene, from, to, target, text, sessionId (scene-target)
  shouldIgnoreNotification: data => {
    log.d('shouldIgnoreNotification', data)
    // [Neo] .TODO always ignore notify
    return true
  },

  shouldCountTeamNotifyUnread: data =>
      log.v('shouldCountTeamNotifyUnread', data),

  onRoamingMsgs: data => {
    log.v('onRoamingMsgs', data)
    foo.msgs2session(currentAccount, data.msgs.map(foo.msgTodo))
  },

  onOfflineMsgs: list => {
    log.v('onOfflineMsgs', list)

    if (Array.isArray(list)) {
      list.forEach(l => foo.msgs2session(currentAccount,
          l.msgs.map(foo.msgTodo)))
    } else {
      foo.msgs2session(currentAccount, list.msgs.map(foo.msgTodo))
    }
  },

  // status
  // scene, from, to, target, sessionId (scene-target)
  // type, text,
  // time, userUpdateTime
  // attach: accounts, members, team, type, users
  onMsg: msg => {
    log.v('onMsg', msg)

    if ('notification' === msg.type) {
      switch (msg.attach.type) {
        case 'addTeamManagers':
          foo.handle(_handler, what.notify,
              arg1.success, arg2.teamAddManagers, foo.teamCbTodo(msg.attach))
          break

        case 'removeTeamManagers':
          foo.handle(_handler, what.notify,
              arg1.success, arg2.teamRemoveManagers, foo.teamCbTodo(msg.attach))
          break

        default:
          break
      }
    }

    let m = foo.msgTodo(msg)

    db.open(imdb.prefix + currentAccount)
    .then(e => db.add(e, imtables.message.name, m))
    .then(e => db.update(e, imtables.session.name, m.session, (v) => {
      if (null == v) {
        v = foo.msg4session(m)
      }

      if (custom.type === m.type && custom.recall === m.custom.type) {
        v.unread -= 1

        if (v.unread < 0) {
          v.unread = 0
        }
      } else if (custom.type === m.type &&
          m.custom.type &&
          m.custom.type.startsWith(custom.secret)) {
        // [Neo] .Empty, not add into read
      } else {
        v.unread += 1
      }

      v.ts = v.ts > m.ts ? v.ts : m.ts
      v.message = m

      // [Neo] .mute filter
      if (!v.mute) {
        if (null != _msg_cb) {
          _msg_cb(m)
        }
      }

      return v
    }))
    .then(_ => foo.handle(_handler, what.receive,
        foo.s2a1(msg), arg2.normal, m))
    .catch(e => log.e(e))
  },

  onSysMsg: data => {
    log.w('onSysMsg', data)
    foo.handle(_handler, what.receive, arg1.success, arg2.system, data)
  },

  onCustomSysMsg: data => {
    log.w('onCustomSysMsg', data);
    foo.handle(_handler, what.receive, arg1.success, arg2.system, data)
  },

  onUpdateSysMsg: data => log.w('onUpdateSysMsg', data),

  // deleteMsg?, msg, total
  onSysMsgUnread: list => log.w('onSysMsgUnread', list),
  onUpdateSysMsgUnread: data => log.v('onUpdateSysMsgUnread', data),

  onRoamingSysMsgs: data => log.w('onRoamingSysMsgs', data),
  onOfflineSysMsgs: data => log.w('onOfflineSysMsgs', data),
  onOfflineCustomSysMsgs: data => {
    log.w('onOfflineCustomSysMsgs', data);
    // foo.handle(_handler, what.receive, arg1.success, arg2.system, data)
  },

  onSyncDone: _ => {
    log.v('onSyncDone');

    synced = true;
    dp.done('im-init', null, nim.session.list())
    foo.handle(_handler, what.synced, arg1.success, arg2.none)
  },
}

// [Neo] .TODO
//   "onofflinefiltermsgs",
//   "onofflinefiltersysmsgs"
//   "onofflinefiltercustomsysmsgs"
//   "onbroadcastmsg"
//   "onbroadcastmsgs",
//   "onTeamMsgReceipt",

const defaultOptions = {
  db: true,
  debug: false,
  secure: true,

  syncMsgReceipts: true,
  syncRoamingMsgs: true,

  autoMarkRead: true,

  // [Neo] .change defaults
  syncSessionUnread: true,

  // [Neo] .change defaults
  syncTeams: false,
  syncRelations: false,

  // [Neo] .deprecated
  // syncTeamMembers: true,

  // logFunc: cb.onLog,

  onsessions: cb.onSessions,
  onupdatesession: cb.onUpdateSession,

  onsysmsg: cb.onSysMsg,
  oncustomsysmsg: cb.onCustomSysMsg,
  onupdatesysmsg: cb.onUpdateSysMsg,
  onroamingsysmsgs: cb.onRoamingSysMsgs,
  onofflinesysmsgs: cb.onOfflineSysMsgs,

  onmsg: cb.onMsg,
  onconnect: cb.onConnect,
  ondisconnect: cb.onDisconnect,
  onteams: cb.onTeams,
  onsysmsgunread: cb.onSysMsgUnread,
  onsyncdone: cb.onSyncDone,
  onAddTeamMembers: cb.onAddTeamMembers,
  onwillreconnect: cb.onWillReconnect,
  onerror: cb.onError,

  onloginportschange: cb.onLoginPortsChange,
  onsynccreateteam: cb.onSyncCreateTeam,
  onteammembers: cb.onTeamMembers,
  onsyncteammembersdone: cb.onSyncTeamMembersDone,
  onupdateteammember: cb.onUpdateTeamMember,
  onCreateTeam: cb.onCreateTeam,
  onUpdateTeam: cb.onUpdateTeam,
  onUpdateTeamManagers: cb.onUpdateTeamManagers,
  onRemoveTeamMembers: cb.onRemoveTeamMembers,
  onUpdateTeamMembers: cb.onUpdateTeamMembers,
  onDismissTeam: cb.onDismissTeam,
  onTransferTeam: cb.onTransferTeam,
  onUpdateTeamMembersMute: cb.onUpdateTeamMembersMute,
  shouldCountTeamNotifyUnread: cb.shouldCountTeamNotifyUnread,
  onroamingmsgs: cb.onRoamingMsgs,
  onofflinemsgs: cb.onOfflineMsgs,
  onupdatesysmsgunread: cb.onUpdateSysMsgUnread,
  onofflinecustomsysmsgs: cb.onOfflineCustomSysMsgs,

  shouldIgnoreNotification: cb.shouldIgnoreNotification,
}

let nim = {
  init: (options, handler) => {
    if (synced && connected) {
      if (options.account === currentAccount) {
        log.w('reinit, just return session.list')
        return nim.session.list()
      } else {
        log.w('reinit different account, disconnect & init')
        return nim.disconnect().then(_ => nim.init(options, handler))
      }
    }

    const key = 'im-init'
    dp.prepare(key)

    let nimOptions = Object.assign({}, defaultOptions, options)
    currentAccount = nimOptions.account

    db.open(imdb.prefix + currentAccount, imtables, imdb.version)
    .then(_ => {
      sdk = NIM.getInstance(nimOptions)
      _handler = handler
    })
    .catch(e => dp.done(key, e))

    return dp.promise(key)
  },

  deinit: _ => {
    if (!synced || !connected) {
      return dp.bad('not inited')
    }

    const key = 'im-deinit'
    dp.prepare(key)

    sdk.destroy({
      done: e => dp.done(key, e)
    })

    return dp.promise(key)
  },

  setHandler: handler => _handler = handler,

  setMsgCb: cb => _msg_cb = cb,

  connect: _ => {
    const key = 'im-connect'
    dp.prepare(key);

    sdk.connect();

    return dp.promise(key)
  },

  disconnect: _ => {
    const key = 'im-disconnect'
    dp.prepare(key);

    sdk.disconnect();

    return dp.promise(key)
  },

  connected: _ => connected,
  synced: _ => synced,

  session: {
    pin: sid => {
      const isArray = Array.isArray(sid)
      const key = isArray
          ? dp.genKey('im-pin-' + sid[0] + '-' + sid.length)
          : dp.genKey('im-pin-' + sid)
      dp.prepare(key)

      db.open(imdb.prefix + currentAccount)
      .then(e => {
        if (isArray) {
          return db.updateBatch(e, imtables.session.name, sid, sid, (v, s) => {
            if (null == v) {
              v = foo.id4session(s)
            }

            v.pin = 1
            v.ts = new Date().getTime()

            return v
          })
        } else {
          return db.update(e, imtables.session.name, sid, v => {
            if (null == v) {
              v = foo.id4session(sid)
            }

            v.pin = 1
            v.ts = new Date().getTime()

            return v
          })
        }

      })
      .then(_ => dp.done(key))
      .catch(e => dp.done(key, e))

      return dp.promise(key)
    },

    unpin: sid => {
      const isArray = Array.isArray(sid)
      const key = isArray
          ? dp.genKey('im-unpin-' + sid[0] + '-' + sid.length)
          : dp.genKey('im-unpin-' + sid)
      dp.prepare(key)

      db.open(imdb.prefix + currentAccount)
      .then(e => {
        if (isArray) {
          return db.updateBatch(e, imtables.session.name, sid, sid, (v, s) => {
            if (null == v) {
              v = foo.id4session(s)
            }

            v.pin = 0
            return v
          })
        } else {
          return db.update(e, imtables.session.name, sid, v => {
            if (null == v) {
              v = foo.id4session(sid)
            }

            v.pin = 0
            return v
          })
        }
      })
      .then(_ => dp.done(key))
      .catch(e => dp.done(key, e))

      return dp.promise(key)
    },

    mute: sid => {
      const isArray = Array.isArray(sid)
      const key = isArray
          ? dp.genKey('im-mute-' + sid[0] + '-' + sid.length)
          : dp.genKey('im-mute-' + sid)
      dp.prepare(key)

      db.open(imdb.prefix + currentAccount)
      .then(e => {
        if (isArray) {
          return db.updateBatch(e, imtables.session.name, sid, sid, (v, s) => {
            if (null == v) {
              v = foo.id4session(s)
            }

            v.mute = 1
            return v
          })
        } else {
          return db.update(e, imtables.session.name, sid, v => {
            if (null == v) {
              v = foo.id4session(sid)
            }

            v.mute = 1
            return v
          })
        }
      })
      .then(_ => dp.done(key))
      .catch(e => dp.done(key, e))

      return dp.promise(key)
    },

    unmute: sid => {
      const isArray = Array.isArray(sid)
      const key = isArray
          ? dp.genKey('im-unmute-' + sid[0] + '-' + sid.length)
          : dp.genKey('im-unmute-' + sid)
      dp.prepare(key)

      db.open(imdb.prefix + currentAccount)
      .then(e => {
        if (isArray) {
          return db.updateBatch(e, imtables.session.name, sid, sid, (v, s) => {
            if (null == v) {
              v = foo.id4session(s)
            }

            v.mute = 0
            return v
          })
        } else {
          return db.update(e, imtables.session.name, sid, v => {
            if (null == v) {
              v = foo.id4session(sid)
            }

            v.mute = 0
            return v
          })
        }
      })
      .then(_ => dp.done(key))
      .catch(e => dp.done(key, e))

      return dp.promise(key)
    },

    read: sid => {
      const key = 'im-session-read-' + sid
      dp.prepare(key)

      sdk.resetSessionUnread(sid)

      db.open(imdb.prefix + currentAccount)
      .then(e => db.update(e, imtables.session.name, sid, v => {
        if (null == v) {
          v = foo.id4session(sid)
        }

        v.unread = 0
        return v
      }))

      .then(e => {
        let r = null

        if (sid.startsWith('p2p')) {
          r = db.lastByIndex(e, imtables.message.name,
              'session', v => (sid === v.session && currentAccount === v.to))
        } else {
          r = dp.good({data: e})
        }

        return r
      })
      .then(e => {
        let r = dp.good({data: e})

        if (e.hasOwnProperty('result') && e.result.length > 0) {
          r = nim.message.read(e.result[0])
        }

        return r
      })

      .then(_ => dp.done(key))

      return dp.promise(key)
    },

    list: _ => {
      const key = dp.genKey('im-session-list-' + currentAccount)
      dp.prepare(key)

      db.open(imdb.prefix + currentAccount)
      .then(e => db.list(e, imtables.session.name, v => v.ts > v.ignore))
      .then(e => {
        let list = e.result
        let length = list.length

        if (0 === list.length) {
          dp.done(key, null, [])
        }

        list.forEach(l => {
          db.lastByIndex(e, imtables.message.name,
              'session', v => l.session === v.session)
          .then(e => {
            if (null != e && e.length > 0) {
              l.message = e[0]
            }
          })
          .then(_ => {
            length -= 1

            if (0 === length) {
              dp.done(key, null, list.sort(foo.sortByPinAndTs))
            }
          })
        })

      })
      .catch(e => dp.done(key, e))

      return dp.promise(key)
    },

    last: sid => {
      const key = 'im-session-last-' + sid
      dp.prepare(key)

      db.open(imdb.prefix + currentAccount)
      .then(e => db.lastByIndex(e, imtables.message.name,
          'session', v => sid === v.session))
      .then(e => dp.done(key, null, e.result))

      return dp.promise(key)
    },

    delete: (sid /*, remote = false*/) => {
      const to = foo.sidTodo(sid)
      const key = dp.genKey('im-session-delete-' + sid)
      dp.prepare(key)

      sdk.deleteLocalSession({
        id: sid,
        done: (err, _) => {
          db.open(imdb.prefix + currentAccount)
          .then(e => db.update(e, imtables.session.name, sid, (v) => {
            if (null == v) {
              v = foo.id4session(sid)
            }

            v.ignore = new Date().getTime()
            return v
          }))
          .then(_ => {
            // if (!remote) {
            dp.done(key, err)
            // }
          })
          .catch(e => log.e(e))
        }
      })

      // if (remote) {
      //   sdk.deleteSession({
      //     scene: to[0],
      //     to: to[1],
      //     done: err => dp.done(key, err, nim.session.list())
      //   })
      // }

      return dp.promise(key)
    },

    clean: (remote = false) => {
      const key = dp.genKey('im-chat-clean')
      dp.prepare(key)

      db.open(imdb.prefix + currentAccount)
      .then(e => db.truncate(e, imtables.message.name))
      .then(e => db.list(e, imtables.session.name))
      .then(e => {
        let list = e.result
        return db.updateBatch(e, imtables.session.name,
            list.map(l => l.session), list, (old, _) => {
              old.unread = 0
              old.ignore = new Date().getTime()
              return old
            })
      })
      .then(_ => {
        if (!remote) {
          dp.done(key)
        }
      })
      .catch(e => dp.done(key, e))

      sdk.deleteAllLocalSysMsgs({
        done: (err, obj) => log.v('clean sys msgs', err, obj)
      })
      sdk.deleteAllLocalMsgs({
        done: (err, obj) => log.v('clean msgs', err, obj)
      })

      if (remote) {
        db.open(imdb.prefix + currentAccount)
        .then(e => db.list(e, imtables.session.name))
        .then(e => {
          const ss = e.result.map(s => {
            const to = foo.sidTodo(s.session)
            return {
              scene: to[0],
              target: to[1],
            }
          })

          sdk.deleteSessions({
            sessions: ss,
            done: (err, obj) => dp.done(key, err, obj)
          })
        }).catch(e => dp.done(key, e))
      }
      return dp.promise(key)
    },
  },

  message: {
    resend: (message) => {
      const key = message.origin.custom[custom.dpKey]
      dp.prepare(key)

      sdk.resendMsg({
        msg: message.origin,
        done: cb.onSendDone
      })

      dp.promise(key)
    },

    send: (sid, message, secret = false) => {
      const key = dp.genKey('im-send')
      dp.prepare(key)

      const to = foo.sidTodo(sid)
      let merge = {
        scene: to[0],
        to: to[1],

        type: ('file' === message.type)
            ? (message.fileType || 'file')
            : message.type,
        content: JSON.stringify(message.content || {}),

        // cc: !secret,
        // isSyncable: !secret,

        isUnreadable: secret,
        needMsgReceipt: ('p2p' === to[0]) ? !secret : false,

        done: cb.onSendDone,
      }

      message.custom = message.custom || {}
      message.custom[custom.dpKey] = key
      message.custom = JSON.stringify(message.custom)

      let msg = Object.assign({}, message, merge)
      switch (message.type) {
        case 'custom':
          sdk.sendCustomMsg(msg)
          break;

        case 'file':
          sdk.sendFile(msg)
          break;

        case 'text':
        default:
          sdk.sendText(msg)
          break;
      }

      return dp.promise(key)
    },

    isRead: message => {
      let result = false

      if (message.session.startsWith('p2p')) {
        result = sdk.isMsgRemoteRead(message.origin)
      }

      return result
    },

    read: (message, deleteAfter) => {
      const key = dp.genKey('im-message-read-' + message.session)
      dp.prepare(key)

      db.open(imdb.prefix + currentAccount)
      // [Neo] .TODO update message read = 0
      .then(e => db.update(e, imtables.session.name, message.session, (v) => {
        if (null == v) {
          v = foo.id4session(message.session)
        }

        v.unread = 0
        return v
      }))
      .catch(e => log.e(e))

      if ('p2p' === message.scene) {
        sdk.sendMsgReceipt({
          msg: message.origin,
          done: (err, obj) => {

            if (deleteAfter > 0) {
              // [Neo] .delete local recv
              nim.message.delete(message)

              let sid = 'p2p-' + message.from
              nim.message.send(sid, {
                type: custom.type,
                custom: {
                  type: custom.secretDeleteMsg,
                  id: message.id,
                },
              }, true)

              for (let i = 0; i < deleteAfter; ++i) {
                setTimeout(_ => {
                  let c = {
                    type: custom.secretCount,
                    id: message.id,
                    count: deleteAfter - i,
                  }

                  foo.handle(_handler, what.notify,
                      arg1.success, arg2.secretCount, c)

                  nim.message.send(sid, {
                    type: custom.type,
                    custom: c,
                  }, true)

                }, i * 1000)
              }

              setTimeout(_ => {
                let c = {
                  type: custom.secretDeleteUI,
                  id: message.id,
                }

                foo.handle(_handler, what.notify,
                    arg1.success, arg2.secretDeleteUI, c)

                nim.message.send(sid, {
                  type: custom.type,
                  custom: c,
                }, true)
                .then(_ => dp.done(key))
                .catch(e => dp.done(key, e))

              }, deleteAfter * 1000)

            } else {
              dp.done(key, err, obj)
            }

          }
        })
      } else {
        dp.done(key, 'not support')
      }

      return dp.promise(key)
    },

    recall: message => {
      return nim.message.delete(message, true, true)
    },

    delete: (message, remote = false, recall = false) => {
      const key = 'im-message-delete-' + message.id
      dp.prepare(key)

      sdk.deleteLocalMsg({
        msg: message.origin,
        done: err => {

          db.open(imdb.prefix + currentAccount)
          .then(e => db.deleteWhenIndex(e, imtables.message.name,
              'id', message.id))
          .then(_ => {
            if (!remote) {
              dp.done(key, err)
            }
          })
          .catch(e => dp.done(key, e))

        }
      })

      if (remote) {
        sdk.deleteMsg({
          msg: message.origin,
          done: err => {

            if (recall) {
              nim.message.send(message.session, {
                type: custom.type,
                custom: {
                  type: custom.recall
                },
              }, true)
              .then(e => dp.done(key, null, e))
              .catch(e => dp.done(key, e))

            } else {
              dp.done(key, err)
            }

          }
        })
      }

      return dp.promise(key)
    },

    deleteById: (id, remote = false) => {
      return db.open(imdb.prefix + currentAccount)
      .then(e => db.getByIndex(e, imtables.message.name, 'id', id))
      .then(e => {
        const msg = e.result
        if (msg && msg.id) {
          nim.message.delete(msg, remote, false)
          .then(_ => dp.good(msg))
          .catch(e => dp.bad(e))
        } else {
          dp.bad('not found: ' + id)
        }
      })
      .catch(e => dp.bad(e))
    },

    forward: (sid, message) => {
      const to = foo.sidTodo(sid)
      const key = 'im-message-forward-' + sid + '-' + message.id
      dp.prepare(key)

      sdk.forwardMsg({
        scene: to[0],
        to: to[1],
        msg: message.origin,
        done: cb.onSendDone
      })

      return dp.promise(key)
    },

    list: (sid, ts = null, limit = 50, asc = true, remote = false) => {
      const to = foo.sidTodo(sid)
      const end = (null === ts) ? new Date().getTime() : ts
      const key = 'im-message-list-' + sid + '-' + end
      dp.prepare(key)

      db.open(imdb.prefix + currentAccount)
      .then(e => db.get(e, imtables.session.name, sid))
      .then(e => {
        const ignore = (null != e.result) ? e.result.ignore : 0
        let done = (err, obj) => dp.done(key, err, !err
            ? obj.msgs.map(foo.msgTodo).filter(m => m.ts > ignore)
            : null)

        if (remote) {
          sdk.getHistoryMsgs({
            scene: to[0],
            to: to[1],
            endTime: end,
            limit: limit,
            asc: asc,
            // [Neo] .TODO merge into local
            done: done,
          })
        } else {
          // sdk.getLocalMsgs({
          //   sessionId: sid,
          //   end: end,
          //   limit: limit,
          //   desc: !asc,
          //   done: done,
          // })

          db.listByIndex(e, imtables.message.name,
              'session', v =>
                  (sid === v.session && v.ts > ignore && v.ts < end)
              , null, null, false, false, limit)
          .then(e => dp.done(key, null, e.result.reverse()))
        }
      })

      return dp.promise(key)
    },

    search: (sid, key) => {
      // [Neo] .TODO local db need

    },

  },

  user: {
    info: account => {
      return userDate.hasOwnProperty(account) ?
          dp.good(userDate[account]) : nim.user.fetch(account)
    },

    fetch: account => {
      const key = dp.genKey('im-user-' + account)
      dp.prepare(key)

      sdk.getUser({
        account: account,
        done: (err, obj) => {
          userDate[obj.account] = obj
          dp.done(key, err, obj)
        }
      })

      return dp.promise(key)
    },

  },

  conference: {
    add: (audio = true, name, users) => {

    },

    ready: sid => {

    },

    quit: sid => {

    },

    list: (page = 1, size = 10) => {

    },
  },

}

export default nim;
